/*
 *  Copyright 2008 Mark Ashworth <javameme@gmail.com>.
 * 
 *  Licensed under the GNU General Public License v3 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.gnu.org/licenses/gpl-3.0.html
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */

package xperiment.metaphor.service.user.impl;

import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.dao.DataAccessException;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.security.Authentication;
import org.springframework.security.event.authentication.AbstractAuthenticationFailureEvent;
import org.springframework.security.event.authentication.AuthenticationFailureBadCredentialsEvent;
import org.springframework.security.event.authentication.AuthenticationFailureConcurrentLoginEvent;
import org.springframework.security.event.authentication.AuthenticationFailureCredentialsExpiredEvent;
import org.springframework.security.event.authentication.AuthenticationFailureDisabledEvent;
import org.springframework.security.event.authentication.AuthenticationFailureExpiredEvent;
import org.springframework.security.event.authentication.AuthenticationFailureLockedEvent;
import org.springframework.security.event.authentication.AuthenticationFailureProviderNotFoundEvent;
import org.springframework.security.event.authentication.AuthenticationFailureProxyUntrustedEvent;
import org.springframework.security.event.authentication.AuthenticationFailureServiceExceptionEvent;
import org.springframework.security.event.authentication.AuthenticationSuccessEvent;
import org.springframework.security.event.authorization.AuthorizationFailureEvent;
import org.springframework.security.event.authorization.AuthorizedEvent;
import org.springframework.security.event.authorization.PublicInvocationEvent;
import org.springframework.security.ui.WebAuthenticationDetails;
import org.springframework.transaction.annotation.Transactional;
import xperiment.metaphor.model.people.user.AuthenticationEvent;
import xperiment.metaphor.model.people.user.AuthorizationEvent;
import xperiment.metaphor.persistence.Dao;
import xperiment.metaphor.service.user.SecurityAuditService;

/**
 * Security audit service
 * @author Mark Ashworth <javameme@gmail.com>
 * @version 1.0.0
 */
@ManagedResource
public class DefaultSecurityAuditService implements SecurityAuditService, ApplicationListener {
    private static final Log logger = LogFactory.getLog(DefaultSecurityAuditService.class);
    
    private boolean auditSuccessfulAuthenticationEvents = true;
    private boolean auditFailedAuthenticationEvents = true;
    private boolean auditSuccessfulAuthorizationEvents = true;
    private boolean auditFailedAuthorizationEvents = true;

    private Dao authenticationEventDao;
    private Dao authorizationEventDao;

    /**
     * The authentication event data access object
     * @return AuthenticationEventDao
     */
    protected Dao getAuthenticationEventDao() {
        return authenticationEventDao;
    }

    /**
     * The authentication event data access object
     * @param authenticationEventDao The new value
     */
    @Required
    public void setAuthenticationEventDao(Dao authenticationEventDao) {
        this.authenticationEventDao = authenticationEventDao;
    }

    /**
     * The authorization event data access object
     * @return AuthorizationEventDao
     */
    protected Dao getAuthorizationEventDao() {
        return authorizationEventDao;
    }

    /**
     * The authorization event data access object
     * @param authorizationEventDao The new value
     */
    @Required
    public void setAuthorizationEventDao(Dao authorizationEventDao) {
        this.authorizationEventDao = authorizationEventDao;
    }
    
    /**
     * Sets whether the service audits successful authentication events
     * @param value The new value
     */
    @Override
    @ManagedAttribute(description="Whether the service audits successful authentication events")
    public void setAuditSuccessfulAuthenticationEvents(boolean value) {
        this.auditSuccessfulAuthenticationEvents = value;
    }
    
    /**
     * Sets whether the service audits failed authentication events
     * @param value The new value
     */
    @Override
    @ManagedAttribute(description="Whether the service audits failed authentication events")
    public void setAuditFailedAuthenticationEvents(boolean value) {
        this.auditFailedAuthenticationEvents = value;
    }
    
    /**
     * Sets whether the service audits successful authorization events
     * @param value The new value
     */
    @Override
    @ManagedAttribute(description="Whether the service audits successful authorization events")
    public void setAuditSuccessfulAuthorizationEvents(boolean value) {
        this.auditSuccessfulAuthorizationEvents = value;
    }
    
    /**
     * Sets whether the service audits failed authorization events
     * @param value The new value
     */
    @Override
    @ManagedAttribute(description="Whether the service audits failed authorization events")
    public void setAuditFailedAuthorizationEvents(boolean value) {
        this.auditFailedAuthorizationEvents = value;
    }
    
    /**
     * Returns whether the service audits successful authentication events
     * @return boolean
     */
    @Override
    public boolean isAuditSuccessfulAuthenticationEvents() {
        return this.auditSuccessfulAuthenticationEvents;
    }
    
    /**
     * Returns whether the service audits failed authentication events
     * @return boolean
     */
    @Override
    public boolean isAuditFailedAuthenticationEvents() {
        return this.auditFailedAuthenticationEvents;
    }
    
    /**
     * Returns whether the service audits successful authorization events
     * @return boolean
     */
    @Override
    public boolean isAuditSuccessfulAuthorizationEvents() {
        return this.auditSuccessfulAuthorizationEvents;
    }
    
    /**
     * Returns whether the service audits failed authorization events
     * @return boolean
     */
    @Override
    public boolean isAuditFailedAuthorizationEvents() {
        return this.auditFailedAuthorizationEvents;
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        
        try {
            if (isAuditSuccessfulAuthenticationEvents() && event instanceof AuthenticationSuccessEvent) {

                onSuccessfulAuthenticationEvent((AuthenticationSuccessEvent) event);

            } else if (isAuditFailedAuthenticationEvents() &&  event instanceof AbstractAuthenticationFailureEvent) {

                onFailedAuthenticationEvent((AbstractAuthenticationFailureEvent) event);

            } else if (isAuditFailedAuthorizationEvents() && event instanceof AuthorizationFailureEvent) {

                onFailedAuthorizationEvent((AuthorizationFailureEvent) event);

            } else if (isAuditSuccessfulAuthorizationEvents() && event instanceof AuthorizedEvent) {

                onSuccessAuthorizationEvent((AuthorizedEvent) event);

            } else if (isAuditSuccessfulAuthorizationEvents() && event instanceof PublicInvocationEvent) {
                /* Ignore */
            }
        } catch (DataAccessException e) {
            logger.warn("Unable to persist security audit event", e);
        }
    }
    
    @Transactional(readOnly=false)
    protected void onSuccessfulAuthenticationEvent(AuthenticationSuccessEvent event) {
    
        Authentication authentication = event.getAuthentication();
        
        if (authentication != null) {
            
            String details = null;
            if (authentication.getDetails() instanceof WebAuthenticationDetails) {
                WebAuthenticationDetails webDetails = (WebAuthenticationDetails) authentication.getDetails();
                details = "RemoteIpAddress: " + webDetails.getRemoteAddress()
                        + "; SessionId: " + webDetails.getSessionId();
            } else if (authentication.getDetails() != null) {
                details = authentication.getDetails().toString();
            }
            
            AuthenticationEvent e = 
                    new AuthenticationEvent(
                        new Date(), 
                        "SUCCESS", 
                        authentication.getName(), 
                        details, 
                        null);
            
            getAuthenticationEventDao().persist(e);
        }
    }
    
    @Transactional(readOnly=false)
    protected void onFailedAuthenticationEvent(AbstractAuthenticationFailureEvent event) {
        
        Authentication authentication = event.getAuthentication();
        if (authentication == null) {
            return;
        }
        
        String exception = null;
        if (event.getException() != null) {
            event.getException().getMessage();
        }
        String details = null;
        if (authentication.getDetails() instanceof WebAuthenticationDetails) {
            WebAuthenticationDetails webDetails = (WebAuthenticationDetails) authentication.getDetails();
            details = "RemoteIpAddress: " + webDetails.getRemoteAddress()
                    + "; SessionId: " + webDetails.getSessionId();
        } else if (authentication.getDetails() != null) {
            details = authentication.getDetails().toString();
        }
        
        String status = "FAILED";
        if (event instanceof AuthenticationFailureBadCredentialsEvent) {
            status = "BAD CREDENTIALS";
        } else if (event instanceof AuthenticationFailureConcurrentLoginEvent) {
            status = "CONCURRENT LOGIN";
        } else if (event instanceof AuthenticationFailureCredentialsExpiredEvent) {
            status = "CREDENTIALS EXPIRED";
        } else if (event instanceof AuthenticationFailureDisabledEvent) {
            status = "DISABLED";
        } else if (event instanceof AuthenticationFailureExpiredEvent) {
            status = "EXPIRED";
        } else if (event instanceof AuthenticationFailureLockedEvent) {
            status = "LOCKED";
        } else if (event instanceof AuthenticationFailureProviderNotFoundEvent) {
            status = "PROVIDER NOT FOUND";
        } else if (event instanceof AuthenticationFailureProxyUntrustedEvent) {
            status = "PROXY UNTRUSTED";
        } else if (event instanceof AuthenticationFailureServiceExceptionEvent) {
            status = "SERVICE EXCEPTION";
        }
        
        AuthenticationEvent e = 
                    new AuthenticationEvent(
                        new Date(), 
                        status, 
                        authentication.getName(), 
                        details, 
                        exception);
        
        getAuthenticationEventDao().persist(e);
    }
    
    @Transactional(readOnly=false)
    protected void onFailedAuthorizationEvent(AuthorizationFailureEvent event) {
        
        Authentication authentication = event.getAuthentication();
        
        String exception = null;
        if (event.getAccessDeniedException() != null) {
            exception = event.getAccessDeniedException().getMessage();
        }
        String configuration = null;
        if (event.getConfigAttributeDefinition() != null) {
            configuration = event.getConfigAttributeDefinition().toString();
        }
        
        AuthorizationEvent e = new AuthorizationEvent();
        e.setUserName(authentication.getName());
        e.setSource((event.getSource() == null ? null : event.getSource().toString()));
        e.setStatus("ACCESS DENIED");
        e.setException(exception);
        e.setConfiguration(configuration);
        e.setEventTime(new Date());
        
        getAuthorizationEventDao().persist(e);
    } 
    
    @Transactional(readOnly=false)
    protected void onSuccessAuthorizationEvent(AuthorizedEvent event) {
        
        Authentication authentication = event.getAuthentication();
        
        String configuration = null;
        if (event.getConfigAttributeDefinition() != null) {
            configuration = event.getConfigAttributeDefinition().toString();
        }
        
        AuthorizationEvent e = new AuthorizationEvent();
        e.setUserName(authentication.getName());
        e.setSource((event.getSource() == null ? null : event.getSource().toString()));
        e.setStatus("SUCCESS");
        e.setConfiguration(configuration);
        e.setEventTime(new Date());
        
        getAuthorizationEventDao().persist(e);
    } 
}
